using System;
using System.Collections.Generic;
using CLELCore;
using CLELCore.Assembly.Descriptors;
using CLELCore.Assembly.Descriptors.ClassDescriptors;
using CLELCore.Assembly.BlobStreams.BlobInfo;
using CLELCore.Assembly.CLELInstructions;
using CLELCore.Assembly.ILGenerator;
using opt.Tools.Branches;

namespace opt.Tools.Methods
{
	public static class InlineMethod
	{
		public static void Inline(CLEL clel,List<CLELInstruction> caller_code,MethodDescriptor caller_md,
			int call_index,List<CLELInstruction> callee_code,MethodDescriptor callee_md)
		{
			if(clel == null)
				throw new ArgumentException("CLEL cannot be null");
			if(caller_code == null)
				throw new ArgumentException("Caller code cannot be null");
			if(caller_md == null)
				throw new ArgumentException("Caller method descriptor cannot be null");
			if(callee_code == null)
				throw new ArgumentException("Callee code cannot be null");
			if(callee_md == null)
				throw new ArgumentException("Callee method descriptor cannot be null");
			if(call_index < 0 || call_index >= caller_code.Count)
				throw new ArgumentException("Call index out of range");
			CLELInstruction call_inst = caller_code[call_index];
			byte call_opcode = call_inst.Opcode;
			if(call_opcode != CLELOpcode.CALL && call_opcode != CLELOpcode.CALLI &&
				call_opcode != CLELOpcode.CALLVIRT && call_opcode != CLELOpcode.JMP)
			{
				throw new ArgumentException("Call index indexes an instruction that is not a method invocation instruction");
			}

			List<CLELInstruction> callee_code_copy = new List<CLELInstruction>(callee_code);

			UpdateBranchesAfterInserts update_caller_branches = new UpdateBranchesAfterInserts(caller_code);

			uint next_caller_local = CLELInstructionTools.NextLocalNum(caller_code);

			MethodLocalsBlobInfo caller_locals_info = caller_md.Locals;
			List<ClassDescriptor> caller_locals = caller_locals_info.Locals;
			
			//byte[] temp = caller_locals_info.MethodLocalsToBytes();
			//for(int k = 0;k < temp.Length;k++)
			//	Console.Write(temp[k]+" ");
			//Console.WriteLine("\n");
			


			//get callee parameters
			List<ParameterDescriptor> callee_parms = callee_md.Parameters;
			
			int callee_parms_count = callee_parms.Count;
			int i;
			CLELInstruction local_store;
			//which callee parameter maps to caller local variable
			Dictionary<UInt32,UInt32> parms_map = new Dictionary<UInt32,UInt32>();
			uint callee_arg_count = 1;
			bool push_this = true;
			if(callee_md.IsStatic)
			{
				callee_arg_count = 0;
				push_this = false;
			}
			
			//for each parameter in the callee
			for(i = 0;i < callee_parms_count;i++)
			{
				//create a local variable to represent the parameter
				if(next_caller_local == 0)
				{
					local_store = new Stloc0();
				}
				else if(next_caller_local == 1)
				{
					local_store = new Stloc1();
				}
				else if(next_caller_local == 2)
				{
					local_store = new Stloc2();
				}
				else if(next_caller_local == 3)
				{
					local_store = new Stloc3();
				}
				else if(next_caller_local < 256)
				{
					local_store = new Stlocs((byte)next_caller_local);
				}
				else
				{
					local_store = new Stloc((ushort)next_caller_local);
				}
				//insert new local instruction before call instruction in caller
				caller_code.Insert(call_index,local_store);
				//add the map of this new local to callee parameter
				parms_map.Add(callee_arg_count,next_caller_local);
				next_caller_local++;
				callee_arg_count++;
				caller_locals.Add(callee_parms[i].Type.Copy());
			}
		
			if(push_this)
			{
				caller_code.Insert(call_index+callee_parms_count,new Pop());
				update_caller_branches.AddInsertPoint(call_index,callee_parms_count+1);
			}
			else
			{
				update_caller_branches.AddInsertPoint(call_index,callee_parms_count);
			}

			UpdateBranchesAfterNoInstMoving update_callee_branches = new UpdateBranchesAfterNoInstMoving(callee_code_copy);
			//rename locals in callee so they don't conflict with locals in caller
			RenameCalleeNewLocals(callee_code_copy,next_caller_local);
			MethodLocalsBlobInfo callee_locals_info = callee_md.Locals;
			List<ClassDescriptor> callee_locals = callee_locals_info.Locals;
			for(i = 0;i < callee_locals.Count;i++)
			{
				caller_locals.Add(callee_locals[i].Copy());
			}

			RenameCalleeArgToLocals(callee_code_copy,parms_map);
			Dictionary<Int32,Int32> ret_indexes = ConvertRetToBranch(callee_code_copy);
			update_callee_branches.UpdateOffsets();
			//remove last ret
			if(callee_code_copy[callee_code_copy.Count-1].Opcode == CLELOpcode.RET)
				callee_code_copy.RemoveAt(callee_code_copy.Count-1);
			//fix ret's -> br's offsets
			FixRetBrOffsets(callee_code_copy,ret_indexes);

			//remove call instruction
			if(push_this)
			{
				caller_code.RemoveAt(call_index+callee_parms_count+1);
				//insert callee code into caller
				caller_code.InsertRange(call_index+callee_parms_count+1,callee_code_copy);
			}
			else
			{
				caller_code.RemoveAt(call_index+callee_parms_count);
				//insert callee code into caller
				caller_code.InsertRange(call_index+callee_parms_count,callee_code_copy);
			}

			update_caller_branches.AddInsertPoint(call_index+callee_parms_count,callee_code_copy.Count-1);

			//fix branches in caller after inserting callee
			update_caller_branches.UpdateOffsets();
			//write new caller to CLEL
			
			//temp = caller_locals_info.MethodLocalsToBytes();
			//for(int k = 0;k < temp.Length;k++)
			//	Console.Write(temp[k]+" ");
			//Console.WriteLine("\n");
			
			clel.SetMethodsCode(caller_md,caller_code,caller_locals_info);
		}

		private static void FixRetBrOffsets(List<CLELInstruction> code,Dictionary<Int32,Int32> ret_indexes)
		{
			int i;
			int code_count = code.Count;
			int offset = 0;
			for(i = code_count-1;i >= 0;i--)
			{
				if(ret_indexes.ContainsKey(i))
				{
					code[i] = new Br(offset);
				}
				offset += code[i].Length;
			}
		}

		private static Dictionary<Int32,Int32> ConvertRetToBranch(List<CLELInstruction> code)
		{
			int i;
			int code_count = code.Count;
			CLELInstruction inst;
			byte opcode;
			Dictionary<Int32,Int32> ret_indexes = new Dictionary<Int32,Int32>();
			//all methods end in a ret instruction, it will be removed, so skip it
			for(i = code_count-2;i >= 0;i--)
			{
				inst = code[i];
				opcode = inst.Opcode;
				if(opcode == CLELOpcode.RET)
				{
					code[i] = new Br(0);
					ret_indexes.Add(i,0);
				}
			}
			return ret_indexes;
		}

		private static void RenameCalleeNewLocals(List<CLELInstruction> code,uint next_caller_local)
		{
			int i,type;
			int code_count = code.Count;
			CLELInstruction inst,temp_inst;
			byte opcode;
			byte fun;
			ObjectModel om;
			UInt32 arg_num;
			Ldlocs ldlocs;
			Ldlocas ldlocas;
			Ldloc ldloc;
			Ldloca ldloca;
			Stlocs stlocs;
			Stloc stloc;
			bool local_inst;
			for(i = 0;i < code_count;i++)
			{
				inst = code[i];
				opcode = inst.Opcode;
				type = 0; //type of local to generate
					//0 == load local
					//1 == load local address
					//2 == store local
				arg_num = 0;
				local_inst = false;
				if(opcode == CLELOpcode.LDLOC0)
				{
					local_inst = true;
					arg_num = 0;
					type = 0;
				}
				else if(opcode == CLELOpcode.LDLOC1)
				{
					local_inst = true;
					arg_num = 1;
					type = 0;
				}
				else if(opcode == CLELOpcode.LDLOC2)
				{
					local_inst = true;
					arg_num = 2;
					type = 0;
				}
				else if(opcode == CLELOpcode.LDLOC3)
				{
					local_inst = true;
					arg_num = 3;
					type = 0;
				}
				else if(opcode == CLELOpcode.STLOC0)
				{
					local_inst = true;
					arg_num = 0;
					type = 2;
				}
				else if(opcode == CLELOpcode.STLOC1)
				{
					local_inst = true;
					arg_num = 1;
					type = 2;
				}
				else if(opcode == CLELOpcode.STLOC2)
				{
					local_inst = true;
					arg_num = 2;
					type = 2;
				}
				else if(opcode == CLELOpcode.STLOC3)
				{
					local_inst = true;
					arg_num = 3;
					type = 2;
				}
				else if(opcode == CLELOpcode.LDLOCS)
				{
					local_inst = true;
					ldlocs = (Ldlocs)inst;
					arg_num = ldlocs.Local;
					type = 0;
				}
				else if(opcode == CLELOpcode.LDLOCAS)
				{
					local_inst = true;
					ldlocas = (Ldlocas)inst;
					arg_num = ldlocas.Local;
					type = 1;
				}
				else if(opcode == CLELOpcode.STLOCS)
				{
					local_inst = true;
					stlocs = (Stlocs)inst;
					arg_num = stlocs.Local;
					type = 2;
				}
				else if(opcode == CLELOpcode.OBJECTMODEL)
				{
					om = (ObjectModel)inst;
					fun = om.Function;
					if(fun == CLELOpcode.LDLOC)
					{
						local_inst = true;
						ldloc = (Ldloc)inst;
						arg_num = ldloc.Local;
						type = 0;
					}
					else if(fun == CLELOpcode.LDLOCA)
					{
						local_inst = true;
						ldloca = (Ldloca)inst;
						arg_num = ldloca.Local;
						type = 1;
					}
					else if(fun == CLELOpcode.STLOC)
					{
						local_inst = true;
						stloc = (Stloc)inst;
						arg_num = stloc.Local;
						type = 2;
					}
				}

				if(local_inst)
				{
					//generate a local with this number
					if(type == 0)
						temp_inst = SingleILGenerator.GenerateLoadLocal(arg_num+next_caller_local);
					else if(type == 1)
						temp_inst = SingleILGenerator.GenerateLoadLocalAddress(arg_num+next_caller_local);
					else //type == 2
						temp_inst = SingleILGenerator.GenerateStoreLocal(arg_num+next_caller_local);
					//replace inst with this new instruction
					code[i] = temp_inst;
				}
			}
		}

		private static void RenameCalleeArgToLocals(List<CLELInstruction> code,Dictionary<UInt32,UInt32> map)
		{
			int i,type;
			int code_count = code.Count;
			CLELInstruction inst,temp_inst;
			byte opcode;
			byte fun;
			ObjectModel om;
			Ldargs ldargs;
			Ldargas ldargas;
			Ldarg ldarg;
			Ldarga ldarga;
			Starg starg;
			bool load_inst;
			UInt32 arg_num;
			UInt32 local_num;
			for(i = 0;i < code_count;i++)
			{
				type = 0; //type of load to generate
					//0 == load
					//1 == load address
					//2 == store
				arg_num = 0;
				load_inst = false;
				inst = code[i];
				opcode = inst.Opcode;
				if(opcode == CLELOpcode.LDARG0)
				{
					arg_num = 0;
					load_inst = true;
				}
				else if(opcode == CLELOpcode.LDARG1)
				{
					arg_num = 1;
					load_inst = true;
				}
				else if(opcode == CLELOpcode.LDARG2)
				{
					arg_num = 2;
					load_inst = true;
				}
				else if(opcode == CLELOpcode.LDARG3)
				{
					arg_num = 3;
					load_inst = true;
				}
				else if(opcode == CLELOpcode.LDARGS)
				{
					ldargs = (Ldargs)inst;
					arg_num = ldargs.Argument;
					load_inst = true;
				}
				else if(opcode == CLELOpcode.LDARGAS)
				{
					ldargas = (Ldargas)inst;
					arg_num = ldargas.Argument;
					type = 1;
					load_inst = true;
				}
				else if(opcode == CLELOpcode.OBJECTMODEL)
				{
					om = (ObjectModel)inst;
					fun = om.Function;
					if(fun == CLELOpcode.LDARG)
					{
						ldarg = (Ldarg)inst;	
						arg_num = ldarg.Argument;
						load_inst = true;
					}
					else if(fun == CLELOpcode.LDARGA)
					{
						ldarga = (Ldarga)inst;
						arg_num = ldarga.Argument;
						type = 1;
						load_inst = true;
					}
					else if(fun == CLELOpcode.STARG)
					{
						starg = (Starg)inst;
						arg_num = starg.Argument;
						type = 2;
						load_inst = true;
					}
				}
				
				if(map.ContainsKey(arg_num) && load_inst)
				{
					local_num = map[arg_num];
					
					//generate a local with this number
					if(type == 0)
						temp_inst = SingleILGenerator.GenerateLoadLocal(local_num);
					else if(type == 1)
						temp_inst = SingleILGenerator.GenerateLoadLocalAddress(local_num);
					else //type == 2
						temp_inst = SingleILGenerator.GenerateStoreLocal(local_num);
					//replace inst with this new instruction
					code[i] = temp_inst;
				}
			}
		}

		/*
		public static void print(List<CLELInstruction> code)
		{
			Console.WriteLine("Start");
			int i;
			CLELInstruction inst;
			int offset = 0;
			for(i = 0;i < code.Count;i++)
			{
				inst = code[i];
				Console.WriteLine(offset+": "+inst);
				offset += inst.Length;
			}
		}
		*/
	}
}
